package wechatcorp.config;

import config.CglibProxy;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.commons.lang3.reflect.MethodUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.type.filter.AssignableTypeFilter;
import processor.AnnotationProcessor;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @author jiang
 */
@Configuration
public class AnnotationsPostProcessor implements BeanPostProcessor, ApplicationRunner, ApplicationContextAware, Ordered {
    private static final Logger log = LoggerFactory.getLogger(AnnotationsPostProcessor.class);

    private AtomicBoolean loadingFinished = new AtomicBoolean(false);

    private ApplicationContext applicationContext;
    private Map<Object, Properties> injectBeans = new ConcurrentHashMap<>();
    private Set<AnnotationProcessor> annotationBeans;

    @PostConstruct
    public void annotationBeans() {
        Set<AnnotationProcessor> annotationBeans = new HashSet<>();
        ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
        provider.addIncludeFilter(new AssignableTypeFilter(AnnotationProcessor.class));
        Set<BeanDefinition> components = provider.findCandidateComponents(AnnotationProcessor.class.getPackage().getName());
        for (BeanDefinition component : components) {
            try {
                Class clazz = Class.forName(component.getBeanClassName());
                AnnotationProcessor annotationProcessor = (AnnotationProcessor) clazz.newInstance();
                annotationBeans.add(annotationProcessor);
            } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
                log.error("== AnnotationProcessor -> {} ==", e.getMessage());
            }
        }
        log.info("== AnnotationProcessor -> {} ==", annotationBeans);
        this.annotationBeans = annotationBeans;
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        AtomicReference<Integer> count = new AtomicReference<>(0);
        Class<?> targetCls = bean.getClass();
        Properties properties = this.new Properties();
        Stream<AnnotationProcessor> stream = annotationBeans.stream();
        Stream<AnnotationProcessor> annotationProcessorStream = stream.filter(item -> AnnotationUtils.findAnnotation(targetCls, item.targetAnnotation()) != null);
        Set<AnnotationProcessor> collect = annotationProcessorStream.collect(Collectors.toSet());
        properties.classSet = new HashSet<>(collect);
        count.set(count.get() + collect.size());
        Field[] declaredFields = targetCls.getDeclaredFields();
        Stream<AnnotationProcessor> stream1 = annotationBeans.stream();
        Stream<AbstractMap.SimpleEntry<AnnotationProcessor, Set<Field>>> simpleEntryStream = stream1.map(item -> {
            Stream<Field> stream2 = Arrays.stream(declaredFields);
            Stream<Field> fieldStream = stream2.filter(it -> AnnotationUtils.findAnnotation(it, item.targetAnnotation()) != null);
            Set<Field> collect2 = fieldStream.collect(Collectors.toSet());
            return new HashMap.SimpleEntry<>(item, collect2);
        });
        Map<AnnotationProcessor, Set<Field>> collect1 = simpleEntryStream.collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue));
        properties.fieldSet = new HashMap<>(collect1);
        properties.fieldSet.values().forEach(s -> count.set(count.get() + s.size()));
        Method[] declaredMethods = targetCls.getDeclaredMethods();
        Stream<AnnotationProcessor> stream2 = annotationBeans.stream();
        Stream<AbstractMap.SimpleEntry<AnnotationProcessor, Set<Method>>> simpleEntryStream1 = stream2.map(item -> {
            Stream<Method> stream4 = Arrays.stream(declaredMethods);
            Stream<Method> methodStream = stream4.filter(it -> {
                Stream<Parameter> stream3 = Arrays.stream(it.getParameters());
                Stream<Parameter> parameterStream = stream3.filter(i -> AnnotationUtils.findAnnotation(i, item.targetAnnotation()) != null);
                Set<Parameter> collect3 = parameterStream.collect(Collectors.toSet());
                return AnnotationUtils.findAnnotation(it, item.targetAnnotation()) != null || !(collect3.isEmpty());
            });
            return new HashMap.SimpleEntry<>(item, methodStream.collect(Collectors.toSet()));
        });
        Map<AnnotationProcessor, Set<Method>> collect2 = simpleEntryStream1.collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue));
        properties.methodSet = new HashMap<>(collect2);
        properties.methodSet.values().forEach(s -> count.set(count.get() + s.size()));

        if (count.get() > 0) {
            injectBeans.put(bean, properties);

            if (properties.methodSet != null && !properties.methodSet.isEmpty()) {
                CglibProxy proxy = new CglibProxy(properties.methodSet.keySet());
                Object o = proxy.newInstall(bean);
                Object newBean = targetCls.cast(o);
                for (Field declaredField : targetCls.getDeclaredFields()) {
                    declaredField.setAccessible(true);
                    try {
                        Object val = declaredField.get(bean);
                        declaredField.set(newBean, val);
                    } catch (IllegalAccessException e) {
                    }
                }
                bean = newBean;
            }
            if ((properties.classSet != null && !properties.classSet.isEmpty()) || (properties.fieldSet != null && !properties.fieldSet.isEmpty())) {
                afterBeanAndProcessorInit(bean, properties);
            }
        }
        return bean;
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        for (AnnotationProcessor annotationBean : annotationBeans) {
            Class<?> aClass = annotationBean.getClass();
            annotationBean.setApplicationContext(applicationContext);
            Field[] declaredFields = aClass.getDeclaredFields();
            Method[] declaredMethods = aClass.getDeclaredMethods();
            for (Field declaredField : declaredFields) {
                Autowired autowired = AnnotationUtils.findAnnotation(declaredField, Autowired.class);
                Object bean = null;
                if (autowired != null) {
                    Class<?> type = declaredField.getType();
                    Map<String, ?> beansOfType = this.applicationContext.getBeansOfType(type);
                    List<?> objects = new ArrayList<>(beansOfType.values());
                    bean = objects.get(0);
                }
                Resource resource = AnnotationUtils.findAnnotation(declaredField, Resource.class);
                if (resource != null) {
                    String beanName = resource.name();
                    if (!"".equals(beanName)) {
                        if (resource.type() != null) {
                            bean = this.applicationContext.getBean(beanName, resource.type());
                        } else {
                            bean = this.applicationContext.getBean(beanName);
                        }
                    } else {
                        Class<?> type = resource.type() != null ? resource.type() : declaredField.getType();
                        Map<String, ?> beansOfType = this.applicationContext.getBeansOfType(type);
                        List<?> objects = new ArrayList<>(beansOfType.values());
                        bean = objects.get(0);
                    }
                }
                if (bean != null) {
                    try {
                        FieldUtils.writeDeclaredField(annotationBean, declaredField.getName(), bean, true);
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
            }
            for (Method declaredMethod : declaredMethods) {
                Parameter[] parameters = declaredMethod.getParameters();
                Autowired autowired = AnnotationUtils.findAnnotation(declaredMethod, Autowired.class);
                Object bean = null;
                if (autowired != null && parameters.length == 1) {
                    Parameter parameter = parameters[0];
                    Map<String, ?> beansOfType = this.applicationContext.getBeansOfType(parameter.getType());
                    List<?> objects = new ArrayList<>(beansOfType.values());
                    bean = objects.get(0);
                }
                Resource resource = AnnotationUtils.findAnnotation(declaredMethod, Resource.class);
                if (resource != null) {
                    String beanName = resource.name();
                    if (!"".equals(beanName)) {
                        if (resource.type() != null) {
                            bean = this.applicationContext.getBean(beanName, resource.type());
                        } else {
                            bean = this.applicationContext.getBean(beanName);
                        }
                    } else if (parameters.length == 1) {
                        Parameter parameter = parameters[0];
                        Class<?> type = resource.type() != null ? resource.type() : parameter.getType();
                        Map<String, ?> beansOfType = this.applicationContext.getBeansOfType(type);
                        List<?> objects = new ArrayList<>(beansOfType.values());
                        bean = objects.get(0);
                    }
                }
                if (bean != null) {
                    try {
                        MethodUtils.invokeMethod(annotationBean, declaredMethod.getName(), bean);
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    } catch (NoSuchMethodException e) {
                        e.printStackTrace();
                    } catch (InvocationTargetException e) {
                        e.printStackTrace();
                    }
                }
            }
            loadingFinished.set(true);
            for (Map.Entry<Object, Properties> objectPropertiesEntry : injectBeans.entrySet()) {
                afterBeanAndProcessorInit(objectPropertiesEntry.getKey(), objectPropertiesEntry.getValue());
            }
        }
    }

    private void afterBeanAndProcessorInit(Object bean, Properties properties) {
        if (loadingFinished.get()) {
            log.info("== afterBeanAndProcessorInit::{bean:" + bean + " ,properties:" + properties + " ==");
            if (properties.classSet != null && !properties.classSet.isEmpty()) {
                properties.classSet.forEach(item -> item.classInit(item.targetAnnotation(), bean));
            }
            if (properties.fieldSet != null && !properties.fieldSet.isEmpty()) {
                properties.fieldSet.forEach((k, v) -> {
                    v.forEach(field -> {
                        try {
                            field.setAccessible(true);
                            Object o1 = k.fieldInit(k.targetAnnotation(), bean, field, field.get(bean));
                            if (o1 != null) {
                                field.set(bean, o1);
                            }
                        } catch (IllegalAccessException e) {
                            e.printStackTrace();
                        }
                    });
                });
            }
        }
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }

    class Properties {
        Set<AnnotationProcessor> classSet;
        Map<AnnotationProcessor, Set<Field>> fieldSet;
        Map<AnnotationProcessor, Set<Method>> methodSet;
    }
}

